package com.gitee.dubboOps.Utils;

import com.gitee.dubboOps.model.MetadataType;
import com.gitee.dubboOps.param.GenericInvokeMetadata;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.taobao.jaket.model.MethodDefinition;
import com.taobao.jaket.model.ServiceDefinition;
import com.taobao.jaket.model.TypeDefinition;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class  GenericInvokeUtils {
    private static Logger logger = LoggerFactory.getLogger(GenericInvokeUtils.class);
    private static Pattern COLLECTION_PATTERN = Pattern.compile("^java\\.util\\..*(Set|List|Queue|Collection|Deque)(<.*>)*$");
    private static Pattern MAP_PATTERN = Pattern.compile("^java\\.util\\..*Map.*(<.*>)*$");

    public static GenericInvokeMetadata getGenericInvokeMetadata(ServiceDefinition serviceDefinition,
                                                                 String methodSignature,
                                                                 MetadataType metadataType) {
        MethodDefinition methodDefinition = findMethodDefinition(serviceDefinition, methodSignature);
        if (methodDefinition == null) {
            return null;
        }

        GenericInvokeMetadata genericInvokeMetadata = new GenericInvokeMetadata();
        Object returnType = generateReturnType(serviceDefinition, methodDefinition, metadataType);
        genericInvokeMetadata.setReturnType(returnType);
        List<Object> parameterTypes = generateParameterTypes(serviceDefinition, methodDefinition, metadataType);
        genericInvokeMetadata.setParameterTypes(parameterTypes);
        genericInvokeMetadata.setSignature(methodSignature);
        return genericInvokeMetadata;
    }

    /**
     * <pre>
     * 将MethodDefiniton对象转化成方法签名字符串，方法签名字符串的格式为：
     *
     *  方法名~参数类型1;参数类型2;参数类型3
     *
     * 无参方法的格式即为 方法名
     * </pre>
     *
     * @param md
     * @return
     */
    public static String convertMethodDef2SignatureStr(MethodDefinition md) {
        if (md == null) {
            return null;
        }
        return MethodSignatureUtils.convertMethod2SignatureStr(md.getName(), md.getParameterTypes());
    }

    /**
     * 若 parameters 中的 class 节点存在 Abc.Def 形式表示的内部类，则转化成 Abc$Def 表示的形式。
     *
     * @param parameters
     * @return
     */
    public static Object[] convertInternalClass(Object[] parameters) {
        if (parameters == null || parameters.length == 0) {
            return parameters;
        }
        Object[] result = new Object[parameters.length];
        try {
            for (int i = 0; i < parameters.length; i++) {
                result[i] = convertTypeObj(parameters[i]);
            }
        } catch (Throwable t) {
            logger.error("try to convert internal class error", t);
            return parameters;
        }
        return result;
    }

    /**
     * 若 parameter 中的 class 节点存在 Abc.Def 形式表示的内部类，则转化成 Abc$Def 表示的形式。
     *
     * @param parameters
     * @return
     */
    private static Object convertTypeObj(Object parameter) {
        if (parameter instanceof Map) {
            Map param = (Map) parameter;
            if (param.get("class") != null) {
                ((Map) parameter).put("class", MethodSignatureUtils.convertInternalClassStr((String)param.get("class")));
            }
            for (Object key : param.keySet()) {
                ((Map) parameter).put(key, convertTypeObj(param.get(key)));
            }
            return parameter;
        }
        if (parameter instanceof List) {
            List<Object> result = new ArrayList<Object>();
            for (Object item : (List) parameter) {
                result.add(convertTypeObj(item));
            }
            return result;
        }
        return parameter;
    }

    static void generateComplexType(ServiceDefinition def, TypeDefinition td, Map<String, Object> map,
                                    MetadataType metadataType, Set<String> resolvedTypes) {
        Map<String, TypeDefinition> properties = td.getProperties();
        if (properties.isEmpty()) {
            logger.warn("unrecognized type: {}", td.getType());
            // FIXME: for unrecognized type, should we simply return without put its type in 'class' key?
        }

        String type = td.getType();
        map.put("class", type);
        if (resolvedTypes.contains(type)) {
            return;
        }
        resolvedTypes.add(type);
        properties.keySet().stream()
                .forEach(key -> generateEnclosedType(def, key, properties.get(key), map, metadataType, resolvedTypes));
    }

    static Object generateType(ServiceDefinition def, String type, MetadataType metadataType, Set<String> resolvedTypes) {
        TypeDefinition td = findTypeDefinition(def, type);
        return generateType(def, td, metadataType, resolvedTypes);
    }

    private static Object generateType(ServiceDefinition def, TypeDefinition td, MetadataType metadataType,
                                       Set<String> resolvedTypes) {
        String type = td.getType();
        return isPrimitiveType(td) ? generate(def, td, metadataType)
                : isArray(td) ? generateArrayType(def, type, metadataType, resolvedTypes)
                : isCollection(td) ? generateCollectionType(def, type, metadataType, resolvedTypes)
                : isMap(td) ? generateMapType(def, td, metadataType, resolvedTypes)
                : isEnum(td) ? generate(def, td, metadataType)
                : isCircularReferenceType(td) ? generate(def, td, metadataType)
                : generateComplexType(def, td, metadataType, resolvedTypes);
    }

    private static void generateEnclosedType(ServiceDefinition def, String key, TypeDefinition enclosedType,
                                             Map<String, Object> map, MetadataType metadataType,
                                             Set<String> resolvedTypes) {
        if (enclosedType.getProperties() == null || enclosedType.getProperties().isEmpty()) {
            map.put(key, generateType(def, enclosedType, metadataType, resolvedTypes));
        } else {
            Map<String, Object> enclosedMap = new HashMap<>();
            map.put(key, enclosedMap);
            generateComplexType(def, enclosedType, enclosedMap, metadataType, resolvedTypes);
        }
    }

    private static Object generate(ServiceDefinition def, TypeDefinition td, MetadataType metadataType) {
        if (metadataType == MetadataType.CLASS_TYPE) {
            return td.getType();
        } else if (metadataType == MetadataType.DEFAULT_VALUE) {
            if (isEnum(td)) {
                return generateEnumType(def, td);
            } else {
                return defaultValue(td.getType());
            }
        } else {
            logger.error("invalid metadata type: {}", metadataType);
            throw new IllegalArgumentException("invalid metadata type " + metadataType);
        }
    }

    private static Object generateComplexType(ServiceDefinition def, TypeDefinition td, MetadataType metadataType,
                                              Set<String> resolvedTypes) {
        Map<String, Object> map = new HashMap<>();
        if (resolvedTypes == Collections.EMPTY_SET) {
            resolvedTypes = new HashSet<>();
        }
        generateComplexType(def, td, map, metadataType, resolvedTypes);
        return map;
    }

    private static Object generateEnumType(ServiceDefinition def, TypeDefinition td) {
        return td.getEnums().get(0);
    }

    private static Object generateArrayType(ServiceDefinition def, String type, MetadataType metadataType,
                                            Set<String> resolvedTypes) {
        type = StringUtils.substringBeforeLast(type, "[]");
        return new Object[]{generateType(def, type, metadataType, resolvedTypes)};
    }

    private static Object generateCollectionType(ServiceDefinition def, String type, MetadataType metadataType,
                                                 Set<String> resolvedTypes) {
        type = StringUtils.substringAfter(type, "<");
        type = StringUtils.substringBefore(type, ">");
        if (StringUtils.isEmpty(type)) {
            // 如果 collection 类型未声明，则生成空 collection
            return new Object[] {};
        }
        return new Object[]{generateType(def, type, metadataType, resolvedTypes)};
    }

    // FIXME: cannot generate correct json string when the key is complex type
    private static Object generateMapType(ServiceDefinition def, TypeDefinition td, MetadataType metadataType,
                                          Set<String> resolvedTypes) {
        String keyType = StringUtils.substringAfter(td.getType(), "<");
        keyType = StringUtils.substringBefore(keyType, ",");
        keyType = StringUtils.strip(keyType);

        // 如果 map 类型未声明，则 class type 为 <Object, Object>; value type 为空 map
        Map<Object, Object> map = new HashMap<>();
        if (StringUtils.isEmpty(keyType)) {
            if (MetadataType.CLASS_TYPE.equals(metadataType)) {
                map.put("java.lang.Object", "java.lang.Object");
            }
        } else {
            // 生成 key 默认值
            Object key = generateType(def, keyType, metadataType, resolvedTypes);

            // 生成 value 默认值
            String valueType = StringUtils.substringAfter(td.getType(), ",");
            valueType = StringUtils.substringBefore(valueType, ">");
            valueType = StringUtils.strip(valueType);
            valueType = StringUtils.isNotEmpty(valueType) ? valueType : "java.lang.Object";
            Object value = generateType(def, valueType, metadataType, resolvedTypes);

            map.put(key, value);
        }
        return map;
    }

    private static Object defaultValue(String type) {
        switch (type) {
            case "byte":
            case "java.lang.Byte":
            case "short":
            case "java.lang.Short":
            case "int":
            case "java.lang.Integer":
            case "long":
            case "java.lang.Long":
                return 0;
            case "float":
            case "java.lang.Float":
            case "double":
            case "java.lang.Double":
                return 0.0;
            case "boolean":
            case "java.lang.Boolean":
                return true;
            case "void":
            case "java.lang.Void":
                return null;
            case "java.lang.String":
                return "";
            case "java.lang.Object":
                return Collections.emptyMap();
            case "java.util.Date":
                return System.currentTimeMillis();
            default:
                return Collections.emptyMap();
        }
    }

    private static List<Object> generateParameterTypes(ServiceDefinition sd, MethodDefinition md,
                                                       MetadataType metadataType) {
        return Stream.of(md.getParameterTypes())
                .map(t -> generateType(sd, t, metadataType, Collections.emptySet()))
                .collect(Collectors.toList());
    }

    private static Object generateReturnType(ServiceDefinition sd, MethodDefinition md, MetadataType metadataType) {
        return generateType(sd, md.getReturnType(), metadataType, Collections.emptySet());
    }

    private static TypeDefinition findTypeDefinition(ServiceDefinition def, String type) {
        return def.getTypes().stream()
                .filter(t -> t.getType().equals(type))
                .findFirst()
                .orElse(new TypeDefinition(type));
    }

    private static boolean isArray(TypeDefinition def) {
        return StringUtils.endsWith(def.getType(), "[]");
    }

    private static boolean isCircularReferenceType(TypeDefinition td) {
        return StringUtils.isNotEmpty(td.get$ref());
    }

    static boolean isCollection(TypeDefinition def) {
        String type = StringUtils.substringBefore(def.getType(), "<");
        Matcher matcher = COLLECTION_PATTERN.matcher(type);
        return matcher.matches();
    }

    private static boolean isEnum(TypeDefinition td) {
        return td.getEnums() != null && !td.getEnums().isEmpty();
    }

    static boolean isMap(TypeDefinition def) {
        String type = StringUtils.substringBefore(def.getType(), "<");
        Matcher matcher = MAP_PATTERN.matcher(type);
        return matcher.matches();
    }

    private static boolean isPrimitiveType(TypeDefinition def) {
        String type = def.getType();
        return type.equals("byte") || type.equals("java.lang.Byte") ||
                type.equals("short") || type.equals("java.lang.Short") ||
                type.equals("int") || type.equals("java.lang.Integer") ||
                type.equals("long") || type.equals("java.lang.Long") ||
                type.equals("float") || type.equals("java.lang.Float") ||
                type.equals("double") || type.equals("java.lang.Double") ||
                type.equals("boolean") || type.equals("java.lang.Boolean") ||
                type.equals("void") || type.equals("java.lang.Void") ||
                type.equals("java.lang.String") ||
                type.equals("java.util.Date") ||
                type.equals("java.lang.Object");
    }

    private static MethodDefinition findMethodDefinition(ServiceDefinition serviceDefinition, String methodSignature) {
        if (serviceDefinition == null) {
            return null;
        }
        Optional<MethodDefinition> typeDefinition = serviceDefinition.getMethods().stream()
                .filter(m -> m.getName().equals(MethodSignatureUtils.findMethodName(methodSignature)) &&
                        Arrays.equals(m.getParameterTypes(), MethodSignatureUtils.findParameterTypes(methodSignature, false)))
                .findFirst();
        return typeDefinition.isPresent() ? typeDefinition.get() : null;
    }

    public static boolean isValidInvokeParam(String invokeParam) {
        if (StringUtils.isBlank(invokeParam)) {
            return false;
        }

        try {
            JsonElement json = new JsonParser().parse(invokeParam);
            if (json != null && json.isJsonArray()) {
                return true;
            }
        } catch (JsonParseException e) {
            logger.error("invalid invoke params: " + invokeParam);
        }

        return false;
    }
}